Skip to content

docs: propose host adapters as a published extension point (ADR-0028/0029/0030) - #131

Open
adrianco wants to merge 1 commit into
pacphi:mainfrom
adrianco:docs/hermes-adrs
Open

docs: propose host adapters as a published extension point (ADR-0028/0029/0030)#131
adrianco wants to merge 1 commit into
pacphi:mainfrom
adrianco:docs/hermes-adrs

Conversation

@adrianco

Copy link
Copy Markdown
Contributor

Documentation only. Three ADRs, all Proposed — no implementation is authorized or claimed. Follows the shape of #112.

The short version

I wanted agentic-kit to manage a fourth agent CLI (Hermes Agent, which is what I use to drive local models). The obvious route was ADR-0017's: an owner module and edits across nine files. Its own references section lists fourteen source files and eight test suites for one host.

That route makes every host your permanent obligation, including hosts you don't run and can't verify. So this proposes the smaller thing instead: publish the seam that already exists.

The seam is already built, and already partly adopted

Reading through 0016/0017/0018, every contract a host adapter needs is already specified and enforced:

Contract Validator
Host descriptor + capabilities validateHostAdapter
Cross-axis invariants validateRegistries (at construction)
Configuration lifecycle validateLifecycleAdapter — detect/plan/apply/verify/undo
Ownership + teardown ownership, mayUndo, undoOwnedValues
Worker execution validateExecutionAdapter, validateWorkerResult
Normalized facts normalizedFacts (schemaVersion: 1, provenance-bearing)
Guidance rows registry(customBlocks) — already user-extensible

And OPENCODE_LIFECYCLE_ADAPTER implements the full five-verb contract, driven from sync.mjs and x/host.mjs through the generic runLifecycle.

What's left is a last mile, and it's small:

  • adapter selection is a named import — runLifecycle({ adapter: OPENCODE_LIFECYCLE_ADAPTER, … })
  • status.mjs hand-rolls a per-host block importing eight functions from lib/opencode.mjs, even though detect already returns normalizedFacts

Both changes delete host-specific code rather than adding it, which is why they seem worth making even if no external adapter is ever registered.

ADR-0029 — the actual ask

An adapter is one module exporting one manifest, validated by the validators above. Four constraints make it safe to publish rather than merely convenient:

  1. Explicit kit.json registration, never naming-convention discovery. Scanning for ak-host-* would make an unrelated npm install sufficient to get third-party code executed inside ak on the next ak status — the fail-open pattern ADR-0023 exists to prevent.
  2. Disclosure, not a sandbox claim. In-process modules can't be sandboxed and the ADR doesn't pretend otherwise; the trust manifest names the package, resolved path, and version before any mutation.
  3. Capability caps — no canBePrimary, aqeProvider, or commandStatusline. Those three carry first-party obligations you can't discharge for code you don't ship. It's exactly the shape OpenCode already occupies, so it's a tested configuration, not new policy.
  4. Fail-closed per adapter. A broken third-party adapter is reported and skipped, never able to brick ak status. Built-ins keep throwing at construction, because a broken built-in is a build error.

Plus contract: 1 and an explicit statement that the surface is unstable while the package is alpha — publishing an extension point acquires an obligation, and that statement is what bounds it.

The package stays zero-runtime-dependency: an adapter is something the user installs and registers, never a dependency of @pacphi/agentic-kit.

ADR-0030 — conformance evidence

A contract never satisfied by code its authors didn't write is a guess. Hermes is a useful first consumer because it's awkward — it breaks five assumptions the built-in hosts share: YAML config, no npm package, plain-text output, no interceptable permission event, no ruflo ENABLE_* flag.

Carrying it needed one widening (a plain-text summary capture alongside createJsonlSummaryCapture) and one guard (npmRoot(host.install.npmPackage) in footprint/install.mjs, since hermes is the first host with no npm package). Everything else fit unmodified.

It would ship as an externally maintained adapter — not vendored here, and not asking you to take on hermes's correctness or NousResearch's release cadence. I'd maintain it.

Two findings are in the ADR because they'd otherwise resurface as bug reports:

  • hermes mcp add isn't safely idempotent — its overwrite prompt defaults to No on EOF (which is what a non-TTY ak sync supplies) and exits zero, so a bare re-add is a silent no-op that reads as convergence.
  • hermes -z sets HERMES_YOLO_MODE=1 by its own headless contract, so unlike OpenCode there's no permission event to intercept and no permission_required result to return. ADR-0030 discloses that at enable time rather than letting a hermes worker appear to carry a guarantee it doesn't have.

ADR-0028 — independent of both

The registry knows exactly one local provider, ollama, while a local model is normally an OpenAI-compatible loopback endpoint (MLX, LM Studio, llama.cpp, vLLM) — frequently under a name the user chose, which no vendor enumeration can cover. One generic local-openai row that deliberately claims less than ollama: no catalogue, no runtime probe, no digest, and no discovery facts this repo hasn't measured.

Useful to the hosts you already ship, and lands on its own merits regardless of what happens to 0029/0030.

Sequencing

docs/HOST-ADAPTER-EXTENSION-PROPOSAL.md §7 lays this out; each step is independently reversible, and declining at ADR-0029 costs nothing already spent. §8 is an honest case against the proposal, including the real cost: a published contract acquires consumers, and consumers constrain refactors.

Happy to take this in a different direction — including "no, absorb hermes in-tree the ADR-0017 way" or "not now" — the ADRs are written to be argued with rather than merged as-is. Unrelated: #130 is a small bug fix that currently blocks new test evidence on Homebrew-node macOS.

markdownlint and internal link checks pass; no source changes in this PR.

…0029/0030)

ADR-0029 asks for one thing: publish the seam that already exists. ADR-0016
specified every contract a host adapter needs, ADR-0017 proved them by using
them, and ADR-0018 generalized execution behind them — sync and host pick
already drive OpenCode through the generic runLifecycle. What is left is a
last mile: adapter selection is a named import, and status hand-rolls a
per-host block importing eight functions from lib/opencode.mjs. Both in-tree
changes delete host-specific code rather than adding it.

The alternative considered and rejected was absorbing each new host in-tree,
as ADR-0017 did for OpenCode. It works, and it is why the contracts exist —
but it makes every host a permanent obligation of whoever maintains this
repository, including hosts they may not run and cannot verify. The first
request for a fourth host is the right moment to decide that once rather than
four times.

Constraints that make the surface safe to publish rather than merely
convenient: explicit kit.json registration, never naming-convention discovery
(an unrelated npm install must not get third-party code executed inside ak);
disclosure rather than a sandbox claim, since in-process adapters cannot be
sandboxed; capability caps on canBePrimary, aqeProvider, and commandStatusline,
matching the shape OpenCode already occupies; fail-closed per adapter, so a
broken third-party adapter cannot brick ak status while built-ins keep throwing
at construction; and contract: 1 with an explicit statement that the surface is
unstable while the package is alpha.

ADR-0030 is the conformance evidence. Hermes Agent breaks five assumptions the
built-in hosts share — YAML config, no npm package, plain-text output, no
interceptable permission event, no ruflo backend flag — and carrying it needed
exactly one widening (a plain-text summary capture alongside the JSONL one) and
one guard (npmRoot on an absent npmPackage). It ships as an externally
maintained adapter, not vendored here.

ADR-0028 is independent of both: the registry knows one local provider,
ollama, while a local model is normally an OpenAI-compatible loopback endpoint,
frequently under a user-chosen name no vendor enumeration can cover.

docs/HOST-ADAPTER-EXTENSION-PROPOSAL.md is the companion product proposal,
following the shape of PR pacphi#112.

All three ADRs are Proposed. No implementation is authorized or claimed.
@pacphi

pacphi commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Thanks — this is one of the most carefully argued proposals this repo has received, and it deserves a response in kind. Before evaluating I verified both sides of it: every claim about this codebase against source, and every claim about Hermes against NousResearch/hermes-agent HEAD (v0.20.0, 2026-08-13 — the files your claims rest on last changed 2026-08-08, so HEAD still matches your basis).

What checks out

Everything load-bearing. On the ak side: adapter selection is indeed a named import at the five call sites, the status.mjs opencode block and unguarded npmRoot are as described, and the validators are genuinely host-agnostic — including a synthetic-host seam you didn't cite (trust-manifest.test.mjs:47-69 already injects a grok host end-to-end through setup disclosure). On the Hermes side, both safety findings are confirmed at source level: the mcp add EOF-cancel does exit zero — in fact it's stronger than you claim, since cmd_mcp swallows every handler return, so no hermes mcp subcommand except install can signal failure via exit code — and -z sets the YOLO/accept-hooks env with the devnull-redirected call tree, final-write stdout channel, and 0/1/2 exit codes exactly as documented. mcp serve is confirmed as a pure messaging bridge (ten tools, none delegation-shaped); I agree the reverse bridge must stay excluded.

What I need changed before this moves

1. ADR-0029 must supersede ADR-0016's closed-registry clause explicitly. ADR-0016 states as a design requirement that the registry is "a closed, validated registry of built-in code, not an arbitrary third-party plugin runtime" (its non-goals restate it). Publishing the seam reverses that decision — which may well be right, but it has to be recorded as a supersession, not routed around.

2. §8's in-tree gate list is materially incomplete. The biggest omission: src/lib/execution/adapters.mjs:20-30 enforces a bidirectional invariant between the routable-host set and a frozen 3-entry adapter map at module import. A kit.json-registered host claiming canRouteActivities: true — which ADR-0030 §2 declares — bricks ak run for everyone at import, not at dispatch. That invariant needs to become "every built-in routable host…" plus a merge seam (ADR-0019's cli_unavailable degradation is the in-corpus precedent for the softer rule). Also missing from §8:

  • ak uninstall bypasses runLifecycle entirely (uninstall.mjs:112-142) — a third-party footprint would persist forever.
  • Setup's permission pass authorizes only claude-host rules; removeUndisclosedPermissions (setup.mjs:120-128) would strip a third-party host's permissions and fail setup.
  • An explicit policy for the attribution surfaces: the usage scorecard collapses unknown hosts into claude (usage-index.mjs:1407-1408), live sessions rewrite them to 'internal', and qe-court's vendorOf maps them to 'unknown' — which can distort vendor-diversity findings in either direction. "Excluded and labeled" is an acceptable policy for all three; discovering them as bugs is not.
  • ~12 test assertions pin the host set (hosts.test.mjs:10-11, providers.test.mjs:222-225, the about-directory parity trio, plus five deepEquals on the DEFAULTS literal), and the test-enforced registry↔directory parity means a registered host with no authored card fails CI.
  • kit.json silently round-trips unknown top-level keys, so hostAdapters on an older ak is a silent no-op — it needs at least a warning story.

3. Hermes factual corrections.

  • "No npm package" → "no official npm package": an unofficial bridge (hermes-agent by wyrtensi, currently v0.20.0) publishes hermes/hermes-agent bins via npm, so detection must not infer identity or version from npm presence.
  • lmstudio is a first-class Hermes provider with its own base-URL normalizer, not an alias onto custom; there is no mlx alias (MLX goes through a custom endpoint, per Hermes's own local-LLM guide) — which, if anything, strengthens ADR-0028's user-named-provider argument.
  • ADR-0028's quoted reference config uses api_mode: openai, which Hermes's _parse_api_mode silently drops — the valid value is chat_completions (newer key spelling: transport). Worth fixing both the citation and, on your machine, the config.
  • --max-turns does exist on hermes chat (combinable with non-interactive -q) — your claim is correct for -z, but say so precisely. Minor: the config dispatcher is cmd_config in hermes_cli/subcommands/config.py, not config_command in hermes_cli/config.py.

Disposition

  • ADR-0028: accept after the api_mode fix, plus a note that ollama projects to AQE while local-openai deliberately doesn't — right call, but it should be stated so it doesn't read as a bug later. The providerEntries per-entry refactor in §4 is correct and needed regardless of the rest of this PR.
  • ADR-0029: directionally yes — amend per (1) and (2), then I'll accept. I'd rather the ADR carry the honest gate list than the honest case-against alone.
  • ADR-0030: sound posture — the disclosure-over-guarantee treatment of -z and the mcp serve exclusion are exactly right; apply (3).

This review also triggered a repo-wide consistency pass on my side: several of the gaps above bite OpenCode today, extension point or not, and I'll sequence that in-tree work separately so your adapter lands against a coherent seam rather than a moving one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants